package replicant2;

import Common.Log;
import Common.Share;

import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class GameServerImpl extends GameServerInterfacePOA {

	private String _serverName;
	private Log _log;
	private Lock _mapLock = new ReentrantLock();
	private UdpGameServer _udpServer = null;

	private final int MAX_USER_LENGTH = 15;
	private final int MIN_USER_LENGTH = 6;

	public static final String DELIMETER = "#";

	private final int PASS_LENGTH = 6;

	// map for ip prefix and server name
	public static final Map<String, String> NameIpTable = new HashMap<String, String>() {
		{
			put("132", "NA");
			put("93", "EU");
			put("182", "AS");
		}
	};

	// map for server name and server udp port
	public static final Map<String, Integer> NameServerPortTable = new HashMap<String, Integer>() {
		{
			put("NA", 7788);
			put("EU", 7789);
			put("AS", 7790);
		}
	};

	// map for server name and client udp port
	public static final Map<String, Integer> NameClientPortTable = new HashMap<String, Integer>() {
		{
			put("NA", 7791);
			put("EU", 7792);
			put("AS", 7793);
		}
	};

	private Map<String, List<Player>> _database = new HashMap<String, List<Player>>();

	private Pattern _pattern;

	private static final String IPADDRESS_PATTERN = "^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\."
			+ "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\."
			+ "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\."
			+ "([01]?\\d\\d?|2[0-4]\\d|25[0-5])$";

	public GameServerImpl(String serverName) {
		_serverName = serverName;
		_pattern = Pattern.compile(IPADDRESS_PATTERN);
		_log = new Log("Server-" + serverName+"replicant2");
	}

	/*
	 * get this server name
	 */
	public String getName() {
		return _serverName;
	}

	/*
	 * start udp server in this RMI server
	 */
	public void startUdpServer() {
		int myport = NameServerPortTable.get(_serverName);
		_udpServer = new UdpGameServer(this, myport);

	}

	/*
	 * close its corersponding udp server
	 */
	public void close() {
		_udpServer.stopServer();
	}

	/*
	 * When a player invokes this method from his/her geo-location through a
	 * client program called PlayerClient, the server associated with this
	 * player (determined by the IPAddress) attempts to create an account with
	 * the information passed if the username does not exist and that the passed
	 * information is valid according to the problem description, and inserts
	 * the account at the appropriate location in the hash table. The server
	 * returns information to the player whether the operation was successful or
	 * not and both the server and the player store this information in their
	 * logs
	 */
	
	@Override
	public String createPlayerAccount(String firstName, String lastName,
			int Age, String userName, String password, String iPAddress) {

		String ret = "1";
		if (userName.length() < MIN_USER_LENGTH
				|| userName.length() > MAX_USER_LENGTH) {
			ret = String
					.format("Can not create user account %s, its length is invalid. actual %d , expect (%d-%d)",
							userName, userName.length(), MIN_USER_LENGTH,
							MAX_USER_LENGTH);
			_log.LogError("createPlayerAccount", ret);
			return "0";
		}
		if (!validateIp(iPAddress)) {
			ret = String.format(
					"Can not create user account, wrong ip address  %s",
					iPAddress);
			_log.LogError("createPlayerAccount", ret);
			return "0";
		}
		if (getPlayer(userName) == null) {
			Player p = new Player();
			p.firstName = firstName;
			p.lastName = lastName;
			p.userName = userName;
			p.password = password;
			p.age = Age;
			p.iPAddress = iPAddress;
			this.addPlayer(p);
		} else {
			ret = String.format(
					"Can not create user account %s, it already exists",
					userName);
			_log.LogError("createPlayerAccount", ret);
			return "0";
		}
		ret = String.format("Successfully create user account %s", userName);
		_log.LogInof("createPlayerAccount", ret);
		return "1";
	}

	/*
	 * When a player invokes this method from his/her geo-location through a
	 * client program called PlayerClient,the server associated with this player
	 * (determined by the IPAddress) attempts to verify if the account exists,
	 * that the password matches the account password and that the account is
	 * not currently signed-in. If theseconditions are met, the server sets the
	 * account status to online and returns a confirmation to the player.
	 * Otherwise a descriptive error is returned. Both the server and the player
	 * store this information in their logs
	 */
	@Override
	public String playerSignIn(String userName, String password,
			String iPAddress) {
		String ret = String.format("Successfully login player %s", userName);
		if (!validateIp(iPAddress)) {
			ret = String.format(
					"Can not signin user account %s, wrong ip address",
					iPAddress);
			_log.LogError("playerSignIn", ret);
			return "0";
		}
		Player p = getPlayer(userName);
		if (p == null) {
			ret = String
					.format("Can not signin user account %s, failed to find the account",
							userName);
			_log.LogError("playerSignIn", ret);
			return "0";
		}
		if (!p.password.equals(password)) {
			ret = String.format(
					"Can not transfer account %s, password doesn't match",
					userName);
			_log.LogError("playerSignIn", ret);
			return "0";
		}
		if (!p.signIn()) {
			ret = String.format(
					"Can not signin user account %s, already signin", userName);
			_log.LogError("playerSignIn", ret);
			return "0";
		}
		_log.LogInof("playerSignIn", ret);
		return "1";
	}

	/*
	 * When a player invokes this method from his/her geo-location through a
	 * client program called PlayerClient, the server associated with this
	 * player (determined by the IPAddress) attempts to verify if the account
	 * exists and that the account is currently signed-in. If these conditions
	 * are met, the server sets the account status to offline and returns a
	 * confirmation to the player. Otherwise a descriptive error is returned.
	 * Both the server and the player store this information in their logs.
	 */
	@Override
	public String playerSignOut(String userName, String iPAddress) {
		String ret = String.format("Successfully logout player %s", userName);
		if (!validateIp(iPAddress)) {
			ret = String.format(
					"Can not signout user account %s, wrong ip address",
					iPAddress);
			_log.LogError("playerSignOut", ret);
			return "0";
		}
		Player p = getPlayer(userName);
		if (p == null) {
			ret = String
					.format("Can not signout user account %s, failed to find the account",
							userName);
			_log.LogError("playerSignOut", ret);
			return "0";
		}
		if (!p.signOut()) {
			ret = String.format(
					"Can not signout user account %s, haven't signin yet",
					userName);
			_log.LogError("playerSignOut", ret);
			return "0";
		}

		_log.LogInof("playerSignOut", ret);
		return "1";
	}

	/*
	 * When a player invokes this method from his/her geo-location through a
	 * client program called PlayerClient, the server associated with this
	 * player (determined by the IPAddress) attempts to verify if the account
	 * exists and that the account is currently signed-in. If these conditions
	 * are met, the server sets the account status to offline and returns a
	 * confirmation to the player. Otherwise a descriptive error is returned.
	 * Both the server and the player store this information in their logs.
	 */
	@Override
	public String getPlayerStatus(String adminUsername, String adminPassword,
			String iPAddress) {

		if (!validateIp(iPAddress)) {
			String ret = String
					.format("Can not get player status, wrong ip address %s",
							iPAddress);
			_log.LogError("getPlayerStatus", ret);
			return "0";
		}

		if (!adminUsername.equals("admin")) {
			String ret = String.format("wrong admin user name");
			_log.LogError("getPlayerStatus", ret);
			return "0";
		}

		if (!adminPassword.equals("admin")) {
			String ret = String.format("wrong admin password");
			_log.LogError("getPlayerStatus", ret);
			return "0";
		}

		String ret = "";

		for (String key : NameServerPortTable.keySet()) {
			if (key.equals(_serverName)) {
				String ss = this.getServerStatus();
				ret =ret + ss;
				
				//System.out.println("frist@"+ret);
				//System.out.println("first#"+ss + ";");
			} else {
				int destport = NameServerPortTable.get(key);
				String ss = getServerinfo(destport);
				//System.out.println("my sername:" + key + ";port" + NameServerPortTable.get(key)+";second receive:"+ ss+";");
				ret = ret + ss;
				//System.out.println("second@"+ret);
				//System.out.println("second@"+ss+ ";");
			}
		}

		return ret;
	}

	/*
	 * When a player invokes this method from his/her geolocation, the server
	 * associated with this player (determined by the OldIPAddress) searches its
	 * hash table to check if the account with this Username exists. If it
	 * exists, the entire account is transferred to the geolocation server
	 * associated with the NewIPAddress. Note that the account should be removed
	 * from the hash table of the current server and should be added to the hash
	 * table of the remote server atomically (that is, both operations should
	 * succeed or none should succeed). The server informs the player whether
	 * the operation was successful or not and both the server and the player
	 * store this information in their logs.
	 */
	@Override
	public String transferAccount(String username, String password,
			String oldIPAddress, String newIPAddress) {
		String ret = String.format("Successfully transferAccount account %s",
				username);
		if (!validateIp(oldIPAddress)) {
			ret = String.format(
					"Can not transfer account due to wrong old ip address",
					oldIPAddress);
			_log.LogError("transferAccount", ret);
			return "0";
		}

		if (!this.validateIpFormat(newIPAddress)) {
			ret = String
					.format("Can not transfer account due to wrong format of new ip address",
							newIPAddress);
			_log.LogError("transferAccount", ret);
			return "0";
		}

		Player p = getPlayer(username);
		if (p == null) {
			ret = String.format(
					"Can not transfer account %s, failed to find the account",
					username);
			_log.LogError("transferAccount", ret);
			return "0";
		}
		if (!p.password.equals(password)) {
			ret = String.format(
					"Can not transfer account %s, password doesn't match",
					username);
			_log.LogError("transferAccount", ret);
			return "0";
		}

		String destServerPrefixIp = getPrefixIp(newIPAddress);
		String destServerName = NameIpTable.get(destServerPrefixIp);
		if (NameIpTable.containsKey(destServerPrefixIp)
				&& !destServerName.equals(_serverName)) {
			int destport = NameServerPortTable.get(destServerName);
			p.iPAddress = newIPAddress;
			if (this.transferAccount(destport, p)) {
				this.removePlayer(username);
			}else{
				p.iPAddress = oldIPAddress;
				ret = String.format(
						"Can not transfer account %s, failed to add new account in remote server",
						username);
				_log.LogError("transferAccount", ret);
				return "0";
			}
		}else{
			ret = String.format(
					"Can not transfer account %s, failed to find remote server with new dest ip %s",
					username,newIPAddress);
			_log.LogError("transferAccount", ret);
			return "0";
		}
		_log.LogInof("transferAccount", ret);
		return "1";
	}

	/*
	 * suspendAccount (AdminUsername, AdminPassword, AdminIP, UsernameToSuspend)
	 * admin invokes this method from his/her geolocation, the server associated
	 * with this admin (determined by the AdminIP), if the credentials are
	 * accepted, searches its hash table to check if the account with this
	 * UsernameToSuspend exists. If it exists, the entire account is removed
	 * from the hash table of this geolocation server. The server informs the
	 * admin whether the operation was successful or not and both the server and
	 * the admin store this information in their logs.
	 */
	@Override
	public String suspendAccount(String adminUsername, String adminPassword,
			String adminIP, String usernameToSuspend) {
		String ret = String.format("Successfully suspendAcount %s",
				adminUsername);
		if (!validateIp(adminIP)) {
			ret = String
					.format("Can not suspendAccount player account, wrong ip address %s",
							adminIP);
			_log.LogError("suspendAccount", ret);
			return "0";
		}

		if (!adminUsername.equals("admin")) {
			ret = String.format("wrong admin user name");
			_log.LogError("suspendAccount", ret);
			return "0";
		}

		if (!adminPassword.equals("admin")) {
			ret = String.format("wrong admin password");
			_log.LogError("suspendAccount", ret);
			return "0";
		}

		ret = this.removePlayer(usernameToSuspend);
		_log.LogInof("suspendAccount", ret);
		return "1";
	}

	/*
	 * thread safe function to get current server status
	 */
	public String getServerStatus() {
		int countSignin = 0;
		int countSignout = 0;

		for (Object value : _database.values()) {
			List<Player> list = (List<Player>) value;
			synchronized (list) {
				for (Player p : list) {
					if (p.getStatus()) {
						countSignin += 1;
					} else {
						countSignout += 1;
					}
				}
			}
		}
		String ret = String.format("%s:%d:%d;", _serverName,
				countSignin, countSignout);
		return ret;
	}

	/*
	 * thread safe function to get the playerlist by usrName
	 */
	private List<Player> getPlayerList(String userName) {
		String tag = userName.substring(0, 1);
		List<Player> ret = null;
		_mapLock.lock();

		if (_database.containsKey(tag)) {
			ret = _database.get(tag);
		} else {
			_database.put(tag, ret = new ArrayList());
		}
		_mapLock.unlock();
		return ret;
	}

	/*
	 * Retrieve particular player from database
	 */
	private Player getPlayer(String userName) {
		Player ret = null;
		List<Player> playerlist = getPlayerList(userName);
		synchronized (playerlist) {
			for (Player p : playerlist) {
				if (p.userName.equals(userName)) {
					ret = p;
					break;
				}
			}
		}

		return ret;
	}

	/*
	 * remove particular player from database
	 */
	private String removePlayer(String userName) {
		String ret = "Successfully remove player: " + userName;
		List<Player> playerlist = getPlayerList(userName);
		Player found = null;
		synchronized (playerlist) {
			for (Player p : playerlist) {
				if (p.userName.equals(userName)) {
					found = p;
					break;
				}
			}
			if (found == null) {
				ret = String
						.format("Failed to remove player %s, it doesn't exist in database",
								userName);
			} else {
				playerlist.remove(found);
			}
		}

		return ret;
	}

	/*
	 * Add this player to the database
	 */
	private void addPlayer(Player player) {
		List<Player> playerlist = getPlayerList(player.userName);
		synchronized (playerlist) {
			playerlist.add(player);
		}
	}

	/*
	 * Validate ip address format, return true if the ip format is xxx.xxx.xxx
	 * else return false
	 */
	public boolean validateIp(final String ip) {
		if (validateIpFormat(ip)) {
			if (NameIpTable.get(getPrefixIp(ip)).equals(_serverName)) {
				return true;
			} else {
				_log.LogError("validateIp", String.format(
						"acutal prefix ip:%s, expect ip:%s", getPrefixIp(ip),
						ip));
			}
		} else {
			_log.LogError("validateIp",
					String.format("wrong ip format actual ip %s", ip));
		}
		return false;
	}

	/*
	 * Validate ip address format, return true if the ip format is xxx.xxx.xxx
	 * else return false
	 */
	private boolean validateIpFormat(final String ip) {
		Matcher matcher = _pattern.matcher(ip);
		return matcher.matches();
	}

	/*
	 * Get the first three position of the ip address
	 */
	private String getPrefixIp(final String ip) {
		int index = ip.indexOf(".");
		return ip.substring(0, index);
	}

	/*
	 * Use this function to connect to other UDP server to get the status of
	 * other server
	 */
	private String getServerinfo(int destPort) {
		String ret = "";
		try {
			int clientport = NameClientPortTable.get(_serverName);
			MyDatagramSocket mySocket = new MyDatagramSocket(clientport);
			String message = "1" + DELIMETER + _serverName + DELIMETER;// String.format("%d",
																		// clientport);
			InetAddress receiverHost = InetAddress.getByName("localhost");
			mySocket.sendMessage(receiverHost, destPort, message);
			ret = mySocket.receiveMessage();
			List<String> al = Arrays.asList(ret.split(Share.DELIMETER));
			ret = al.get(0);
			mySocket.close();
		} // end try
		catch (Exception ex) {
			ex.printStackTrace();
		}

		return ret;
	}

	/*
	 * Use this function to connect to other UDP server to get the status of
	 * other server
	 */
	private boolean transferAccount(int destPort, Player p) {
		boolean ret = false;
		try {
			int clientport = NameClientPortTable.get(_serverName);
			MyDatagramSocket mySocket = new MyDatagramSocket(clientport);
			String message = "2" + DELIMETER + _serverName + DELIMETER
					+ p.serilizeToString(p) + DELIMETER;// String.format("%d",
														// clientport);
			InetAddress receiverHost = InetAddress.getByName("localhost");
			mySocket.sendMessage(receiverHost, destPort, message);
			String msg = mySocket.receiveMessage();
			if (msg.charAt(0) == '1') {
				ret = true;
			}
			mySocket.close();
		} // end try
		catch (Exception ex) {
			ex.printStackTrace();
		}

		return ret;
	}

}
